﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using Mpfr.Structs;
using NumberInterface;

namespace Mpfr
{
  public class MpfrNumber : INumber, IDisposable
  {
    static MpfrNumber()
    {
      MpfrImport.set_default_prec(_defaultPrec);
    }

    public MpfrNumber()
    {
      _prec = _defaultPrec;
      _t = new mpfr_t();
      MpfrImport.init2(_t, _prec);
    }

    public MpfrNumber(MpfrNumber op)
    {
      _t = new mpfr_t();
      _prec = _defaultPrec;
      MpfrImport.init_set(_t, op._t, mpfr_rnd_t.GMP_RNDN);
    }

    ~MpfrNumber()
    {
      Dispose();
    }

    protected static uint _defaultPrec = 10;
    public static uint DefaultPrecision
    {
      get { return _defaultPrec; }
      set
      {
        if (_defaultPrec != value)
        {
          _defaultPrec = value;
          MpfrImport.set_default_prec(_defaultPrec);
        }
      }
    }

    protected uint _prec;
    /// <summary>
    /// Warning: setting Precision will erase the value - use RoundPrecision to save the value.
    /// the equation that I calculated for prec (which I am pretty sure is right) is:
    /// prec = 6.1 - log2(epsilon)
    /// 
    /// So, if you want to have a prec value that can handle changes as small as 1e-10, then you need a prec of:    
    /// 6.1 - Log2(1e-10) = 6.1 + 33.2 = 39.3 = (or to be safe, 40)
    /// </summary>
    public uint Precision
    {
      get { return _prec; }
      set
      {
        if (_prec != value)
        {
          _prec = value;
          MpfrImport.set_prec(_t, _prec);
        }
      }
    }
    /// <summary>
    /// Changes the Prec of a mpfrNumber.
    /// 
    /// the equation that I calculated for prec (which I am pretty sure is right) is:
    /// prec = 6.1 - log2(epsilon)
    /// 
    /// So, if you want to have a prec value that can handle changes as small as 1e-10, then you need a prec of:    
    /// 6.1 - Log2(1e-10) = 6.1 + 33.2 = 39.3 = (or to be safe, 40)
    /// </summary>
    /// <param name="prec">the new precision value</param>
    public void RoundPrecision(uint prec)
    {
      MpfrImport.prec_round(_t, prec, mpfr_rnd_t.GMP_RNDN);
      _prec = prec;
    }
    public void FixPrecision()
    {
        if (_prec != _defaultPrec)
        {
            RoundPrecision(_defaultPrec);
        }
    }

    protected string _readFormat = "%Re";//"%.200Rg";
    protected int _numberBase = 10;

    protected mpfr_t _t;

    public void Dispose()
    {
      if (_t != null)
      {
        _t.Dispose();
        _t = null;
      }
    }

    public INumber Assign(double op)
    {
      MpfrImport.set_d(_t, op, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Assign(int op)
    {
      MpfrImport.set_si(_t, op, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Assign(string op)
    {
      MpfrImport.set_str(_t, op, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Assign(uint op)
    {
      MpfrImport.set_ui(_t, op, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Assign(INumber op)
    {
      MpfrImport.set(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public int Compare(INumber op)
    {
      return MpfrImport.cmp(_t, ((MpfrNumber)op)._t);
    }

    public int Compare(double op)
    {
      return MpfrImport.cmp_d(_t, op);
    }

    public int Compare(int op)
    {
      return MpfrImport.cmp_si(_t, op);
    }

    public int Compare(uint op)
    {
      return MpfrImport.cmp_ui(_t, op);
    }

    public bool IsInf()
    {
      return MpfrImport.inf_p(_t) != 0;
    }

    public bool IsNan()
    {
      return MpfrImport.nan_p(_t) != 0;
    }

    public bool IsNumber()
    {
      return MpfrImport.number_p(_t) != 0;
    }

    public bool IsZero()
    {
      return MpfrImport.zero_p(_t) != 0;
    }

    public string Data
    {
      get { return MpfrImport.asprintf(_readFormat, _t); }
      set { MpfrImport.set_str(_t, value, _numberBase, mpfr_rnd_t.GMP_RNDN); }
    }

    public INumber Copy()
    {
      return new MpfrNumber(this);
    }

    public INumber Swap(INumber op)
    {
      MpfrImport.swap(_t, ((MpfrNumber)op)._t);
      return this;
    }

    public double ToDouble()
    {
      return MpfrImport.get_d(_t, mpfr_rnd_t.GMP_RNDN);
    }

    public int ToInt()
    {
      return MpfrImport.get_si(_t, mpfr_rnd_t.GMP_RNDN);
    }

    public uint ToUint(bool roundUp = false)
    {
      return MpfrImport.get_ui(_t, (roundUp ? mpfr_rnd_t.GMP_RNDU : mpfr_rnd_t.GMP_RNDN));
    }

    public override string ToString()
    {
      return Data;
    }

    public INumber Add(INumber op)
    {
      MpfrImport.add(_t, _t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Add(double op)
    {
      MpfrImport.add_d(_t, _t, op, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Add(int op)
    {
      MpfrImport.add_si(_t, _t, op, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Subtract(INumber op)
    {
      MpfrImport.sub(_t, _t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Subtract(double op)
    {
      MpfrImport.sub_d(_t, _t, op, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Subtract(int op)
    {
      MpfrImport.sub_si(_t, _t, op, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Multiply(INumber op)
    {
      MpfrImport.mul(_t, _t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Multiply(double op)
    {
      MpfrImport.mul_d(_t, _t, op, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Multiply(int op)
    {
      MpfrImport.mul_si(_t, _t, op, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Divide(INumber op)
    {
      MpfrImport.div(_t, _t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Divide(double op)
    {
      MpfrImport.div_d(_t, _t, op, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Divide(int op)
    {
      MpfrImport.div_si(_t, _t, op, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber MultiplyAdd(INumber mul, INumber add)
    {
      MpfrImport.fma(_t, _t, ((MpfrNumber)mul)._t, ((MpfrNumber)add)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber MultiplySubtract(INumber mul, INumber sub)
    {
      MpfrImport.fms(_t, _t, ((MpfrNumber)mul)._t, ((MpfrNumber)sub)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Negate()
    {
      MpfrImport.neg(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Power(INumber op)
    {
      MpfrImport.pow(_t, _t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    private static MpfrNumber _pow_d = new MpfrNumber();
    /// <summary>
    /// Warning, this has to convert the double to a mpfrnumber. This will be slow.
    /// </summary>
    /// <param name="op"></param>
    /// <returns></returns>
    public INumber Power(double op)
    {
      _pow_d.Assign(op);
      MpfrImport.pow(_t, _t, _pow_d._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Power(int op)
    {
      MpfrImport.pow_si(_t, _t, op, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Sqrt()
    {
      MpfrImport.sqrt(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Square()
    {
      MpfrImport.sqr(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Add(INumber op1, INumber op2)
    {
      MpfrImport.add(_t, ((MpfrNumber)op1)._t, ((MpfrNumber)op2)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Add(INumber op1, double op2)
    {
      MpfrImport.add_d(_t, ((MpfrNumber)op1)._t, op2, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Add(INumber op1, int op2)
    {
      MpfrImport.add_si(_t, ((MpfrNumber)op1)._t, op2, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Subtract(INumber op1, INumber op2)
    {
      MpfrImport.sub(_t, ((MpfrNumber)op1)._t, ((MpfrNumber)op2)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Subtract(INumber op1, double op2)
    {
      MpfrImport.sub_d(_t, ((MpfrNumber)op1)._t, op2, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Subtract(INumber op1, int op2)
    {
      MpfrImport.sub_si(_t, ((MpfrNumber)op1)._t, op2, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Subtract(double op1, INumber op2)
    {
      MpfrImport.d_sub(_t, op1, ((MpfrNumber)op2)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Subtract(int op1, INumber op2)
    {
      MpfrImport.si_sub(_t, op1, ((MpfrNumber)op2)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }


    public INumber Multiply(INumber op1, INumber op2)
    {
      MpfrImport.mul(_t, ((MpfrNumber)op1)._t, ((MpfrNumber)op2)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Multiply(INumber op1, double op2)
    {
      MpfrImport.mul_d(_t, ((MpfrNumber)op1)._t, op2, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Multiply(INumber op1, int op2)
    {
      MpfrImport.mul_si(_t, ((MpfrNumber)op1)._t, op2, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Divide(INumber op1, INumber op2)
    {
      MpfrImport.div(_t, ((MpfrNumber)op1)._t, ((MpfrNumber)op2)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Divide(INumber op1, double op2)
    {
      MpfrImport.div_d(_t, ((MpfrNumber)op1)._t, op2, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Divide(INumber op1, int op2)
    {
      MpfrImport.div_si(_t, ((MpfrNumber)op1)._t, op2, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Divide(double op1, INumber op2)
    {
      MpfrImport.d_div(_t, op1, ((MpfrNumber)op2)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Divide(int op1, INumber op2)
    {
      MpfrImport.si_div(_t, op1, ((MpfrNumber)op2)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber MultiplyAdd(INumber mul1, INumber mul2, INumber add)
    {
      MpfrImport.fma(_t, ((MpfrNumber)mul1)._t, ((MpfrNumber)mul2)._t, ((MpfrNumber)add)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber MultiplySubtract(INumber mul1, INumber mul2, INumber sub)
    {
      MpfrImport.fms(_t, ((MpfrNumber)mul1)._t, ((MpfrNumber)mul2)._t, ((MpfrNumber)sub)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Negate(INumber op)
    {
      MpfrImport.neg(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Power(INumber op1, INumber op2)
    {
      MpfrImport.pow(_t, ((MpfrNumber)op1)._t, ((MpfrNumber)op2)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    /// <summary>
    /// Warning, this has to convert the double to a mpfrnumber. This will be slow.
    /// </summary>
    /// <param name="op1"></param>
    /// <param name="op2"></param>
    /// <returns></returns>
    public INumber Power(INumber op1, double op2)
    {
      _pow_d.Assign(op2);
      MpfrImport.pow(_t, ((MpfrNumber)op1)._t, _pow_d._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Power(INumber op1, int op2)
    {
      MpfrImport.pow_si(_t, ((MpfrNumber)op1)._t, op2, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Sqrt(INumber op)
    {
      MpfrImport.sqrt(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Square(INumber op)
    {
      MpfrImport.sqr(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Sin()
    {
      MpfrImport.sin(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Cos()
    {
      MpfrImport.cos(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Tan()
    {
      MpfrImport.tan(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber ASin()
    {
      MpfrImport.asin(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber ACos()
    {
      MpfrImport.acos(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber ATan()
    {
      MpfrImport.atan(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber ATan2(INumber y, INumber x)
    {
      MpfrImport.atan2(_t, ((MpfrNumber)y)._t, ((MpfrNumber)x)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Sinh()
    {
      MpfrImport.sinh(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Cosh()
    {
      MpfrImport.cosh(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Tanh()
    {
      MpfrImport.tanh(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Sec()
    {
      MpfrImport.sec(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Csc()
    {
      MpfrImport.csc(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Cot()
    {
      MpfrImport.cot(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Sech()
    {
      MpfrImport.sech(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Csch()
    {
      MpfrImport.csch(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Coth()
    {
      MpfrImport.coth(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber ASinh()
    {
      MpfrImport.asinh(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber ACosh()
    {
      MpfrImport.acosh(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber ATanh()
    {
      MpfrImport.atanh(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Sin(INumber op)
    {
      MpfrImport.sin(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Cos(INumber op)
    {
      MpfrImport.cos(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Tan(INumber op)
    {
      MpfrImport.tan(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber ASin(INumber op)
    {
      MpfrImport.asin(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber ACos(INumber op)
    {
      MpfrImport.acos(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber ATan(INumber op)
    {
      MpfrImport.atan(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Sinh(INumber op)
    {
      MpfrImport.sinh(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Cosh(INumber op)
    {
      MpfrImport.cosh(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Tanh(INumber op)
    {
      MpfrImport.tanh(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Sec(INumber op)
    {
      MpfrImport.sec(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Csc(INumber op)
    {
      MpfrImport.csc(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Cot(INumber op)
    {
      MpfrImport.cot(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Sech(INumber op)
    {
      MpfrImport.sech(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Csch(INumber op)
    {
      MpfrImport.csch(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Coth(INumber op)
    {
      MpfrImport.coth(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber ASinh(INumber op)
    {
      MpfrImport.asinh(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber ACosh(INumber op)
    {
      MpfrImport.acosh(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber ATanh(INumber op)
    {
      MpfrImport.atanh(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Abs()
    {
      MpfrImport.abs(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Ln()
    {
      MpfrImport.log(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Ln1p()
    {
      MpfrImport.log1p(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Log10()
    {
      MpfrImport.log10(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Log2()
    {
      MpfrImport.log2(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber SetE()
    {
      MpfrImport.const_euler(_t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber SetEPow(INumber pow)
    {
      MpfrImport.exp(_t, _t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber SetPI()
    {
      MpfrImport.const_pi(_t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Abs(INumber op)
    {
      MpfrImport.abs(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Hypot(INumber op1, INumber op2)
    {
      MpfrImport.hypot(_t, ((MpfrNumber)op1)._t, ((MpfrNumber)op2)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Ln(INumber op)
    {
      MpfrImport.log(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Ln1p(INumber op)
    {
      MpfrImport.log1p(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Log10(INumber op)
    {
      MpfrImport.log10(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public INumber Log2(INumber op)
    {
      MpfrImport.log2(_t, ((MpfrNumber)op)._t, mpfr_rnd_t.GMP_RNDN);
      return this;
    }

    public void SinCos(ref INumber sop, ref INumber cop)
    {
      MpfrImport.sin_cos(((MpfrNumber)sop)._t, ((MpfrNumber)cop)._t, _t, mpfr_rnd_t.GMP_RNDN);
    }

    public int CompareTo(object obj)
    {
        if (obj is INumber)
        {
            return this.Compare((INumber)obj);
        }
        else if (obj is double)
        {
            return this.Compare((double)obj);
        }
        else if (obj is int)
        {
            return this.Compare((int)obj);
        }
        else if (obj is long)
        {
            return this.Compare((long)obj);
        }
        else if (obj is uint)
        {
            return this.Compare((uint)obj);
        }
        else if (obj is string)
        {
            return this.Compare(new MpfrNumber().Assign((string)obj));
        }
        else
        {
            throw new NotImplementedException();
        }
    }
  }
}
